package me.yokeyword.fragmentation;

import me.yokeyword.fragmentation.extensions.FractionEx;
import me.yokeyword.fragmentation.extensions.FractionManagerEx;
import me.yokeyword.fragmentation.extensions.FractionSchedulerEx;
import me.yokeyword.fragmentation.helper.internal.ResultRecord;
import me.yokeyword.fragmentation.queue.Action;
import me.yokeyword.fragmentation.queue.ActionQueue;
import me.yokeyword.fragmentation.util.LogUtil;

import ohos.aafwk.ability.fraction.Fraction;
import ohos.aafwk.ability.fraction.FractionAbility;
import ohos.aafwk.ability.fraction.FractionScheduler;
import ohos.agp.components.Component;
import ohos.agp.components.ComponentContainer;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.utils.PacMap;
import ohos.utils.Sequenceable;

import java.util.List;
import java.util.Optional;

/**
 * Controller
 */
class TransactionDelegate {
    static final String TAG = TransactionDelegate.class.getSimpleName();
    static final int DEFAULT_POPTO_ANIM = Integer.MAX_VALUE;

    static final String FRAGMENTATION_ARG_RESULT_RECORD = "fraction_arg_result_record";
    static final String FRAGMENTATION_ARG_RESULT_ASKING_FRACTION = "fraction_arg_result_record_ask_fraction";
    static final String FRAGMENTATION_ARG_ROOT_STATUS = "fractionation_arg_root_status";
    static final String FRAGMENTATION_ARG_CONTAINER = "fractionation_arg_container";
    static final String FRAGMENTATION_ARG_REPLACE = "fractionation_arg_replace";
    static final String FRAGMENTATION_STATE_SAVE_IS_HIDDEN = "fractionation_state_save_status";

    private static final String FRAGMENTATION_STATE_SAVE_RESULT = "fractionation_state_save_result";

    static final int TYPE_ADD = 0;
    static final int TYPE_ADD_RESULT = 1;
    static final int TYPE_ADD_WITHOUT_HIDE = 2;
    static final int TYPE_ADD_RESULT_WITHOUT_HIDE = 3;
    static final int TYPE_REPLACE = 10;
    static final int TYPE_REPLACE_DONT_BACK = 11;

    private ISupportAbility mSupport;
    private FractionAbility mAbility;

    private EventHandler mHandler;

    ActionQueue mActionQueue;

    TransactionDelegate(ISupportAbility support) {
        this.mSupport = support;
        this.mAbility = (FractionAbility) support;
        mHandler = new EventHandler(EventRunner.getMainEventRunner());
        mActionQueue = new ActionQueue(mHandler);
    }

    void post(final Runnable runnable) {
        mActionQueue.enqueue(new Action() {
            @Override
            public void run() {
                runnable.run();
            }
        });
    }

    void loadRootTransaction(final FractionManagerEx fm, final int containerId, final ISupportFraction to, final boolean addToBackStack, final boolean allowAnimation) {
        enqueue(fm, new Action(Action.ACTION_LOAD) {
            @Override
            public void run() {
                FractionEx toex = fm.checkNewRecordOrAdd((Fraction) to);
                bindContainerId(containerId, toex);

                String toFactionTag = to.getClass().getName();
                start(fm, null, to, toFactionTag, !addToBackStack, TYPE_REPLACE);
            }
        });
    }

    /**
     * Dispatch the start transaction.
     * @param fm Fraction Manager instance
     * @param requestCode Fraction task Request code
     * @param to To Fraction
     * @param from From Fraction
     * @param launchMode Launch modes SINGLETOP/SINGLETASK
     * @param type Fraction add type
     */
    void dispatchStartTransaction(final FractionManagerEx fm, final ISupportFraction from, final ISupportFraction to, final int requestCode, final int launchMode, final int type) {
        to.getSupportDelegate().setFractionManagerEx(fm);
        enqueue(fm, new Action(launchMode == ISupportFraction.LAUNCHMODE_SINGLETASK ? Action.ACTION_POP_MOCK : Action.ACTION_NORMAL) {
            @Override
            public void run() {
                doDispatchStartTransaction(fm, from, to, requestCode, launchMode, type);
                fm.removeDummyEntries();
            }
        });
    }

    /**
     * Show showFaction then hide hideFaction
     * @param fm Fraction manager
     * @param hideFaction Fraction to Hide
     * @param showFaction Fraction to show
     */
    void showHideFraction(final FractionManagerEx fm, final ISupportFraction showFaction, final ISupportFraction hideFaction) {
        enqueue(fm, new Action() {
            @Override
            public void run() {
                doShowHideFraction(fm, showFaction, hideFaction);
            }
        });
    }

    /**
     * Start the target Fraction and pop itself
     * @param fm fraction manager instance
     * @param to fraction to be started
     * @param from fraction to be removed
     */
    void startWithPop(final FractionManagerEx fm, final ISupportFraction from, final ISupportFraction to) {
        enqueue(fm, new Action(Action.ACTION_POP_MOCK) {
            @Override
            public void run() {
                ISupportFraction top = getTopFractionForStart(from, fm);
                if (top == null)
                    throw new NullPointerException("There is no Fraction in the FractionManagerEx, maybe you need to call loadRootFaction() first!");

                int containerId = top.getSupportDelegate().mContainerId;
                FractionEx toex = fm.checkNewRecordOrAdd((Fraction) to);
                bindContainerId(containerId, toex);
                removeTopFraction(fm);
            }
        });

        dispatchStartTransaction(fm, from, to, 0, ISupportFraction.LAUNCHMODE_STANDARD, TransactionDelegate.TYPE_ADD);
    }

    void startWithPopTo(final FractionManagerEx fm, final ISupportFraction from, final ISupportFraction to, final String fractionTag, final boolean includeTargetFraction) {
        enqueue(fm, new Action(Action.ACTION_POP_MOCK) {
            @Override
            public void run() {
                int flag = 0;
                List<Fraction> willPopFractions = SupportHelper.getWillPopFractions(fm, fractionTag, includeTargetFraction);

                final ISupportFraction top = getTopFractionForStart(from, fm);
                if (top == null)
                    throw new NullPointerException("There is no Fraction in the FractionManagerEx, maybe you need to call loadRootFraction() first!");

                int containerId = top.getSupportDelegate().mContainerId;
                FractionEx toex = fm.checkNewRecordOrAdd((Fraction) to);
                bindContainerId(containerId, toex);

                if (willPopFractions.size() <= 0) return;

                safePopTo(fm, willPopFractions);
            }

        });

        dispatchStartTransaction(fm, from, to, 0, ISupportFraction.LAUNCHMODE_STANDARD, TransactionDelegate.TYPE_ADD);
    }


    /**
     * Remove
     * @param fm fraction manager instance
     * @param fraction fraction instance to remove
     * @param showPreFraction Resume Fraction of same type on removal
     */
    void remove(final FractionManagerEx fm, final FractionEx fraction, final boolean showPreFraction) {
        enqueue(fm, new Action(Action.ACTION_POP, fm) {
            @Override
            public void run() {
                FractionSchedulerEx ft = fm.startFractionScheduler()
                        .remove(fraction);
                supportCommit(ft);
            }
        });
    }

    /**
     * Pop
     * @param fm fraction manager instance
     */
    void pop(final FractionManagerEx fm) {
        enqueue(fm, new Action(Action.ACTION_POP, fm) {
            @Override
            public void run() {
                removeTopFraction(fm);
            }
        });
    }

    private void removeTopFraction(FractionManagerEx fm) {
        try { // Safe popBackStack()
            ISupportFraction top = SupportHelper.getBackStackTopFraction(fm);
            if (top != null) {
                fm.startFractionScheduler()
                        .remove((Fraction) top)
                        .submit();
            }
            FractionEx topactive = SupportHelper.getBackStackTopFractionEx(fm);
            if (top != null) {
                fm.startFractionScheduler()
                        .show(topactive)
                        .submit();
            }
        } catch (Exception ignored) {

        }
    }

    /**
     * Pop the last fraction transition from the manager's fraction pop stack.
     *
     * @param targetFractionTag     Tag
     * @param includeTargetFraction Whether it includes targetFraction
     * @param fm fraction manager
     * @param popAnim animate while fraction transaction
     * @param afterPopTransactionRunnable Runnable Task after fraction transaction
     */
    void popTo(final String targetFractionTag, final boolean includeTargetFraction, final Runnable afterPopTransactionRunnable, final FractionManagerEx fm) {
        enqueue(fm, new Action(Action.ACTION_POP_MOCK) {
            @Override
            public void run() {
                doPopTo(targetFractionTag, includeTargetFraction, fm);

                if (afterPopTransactionRunnable != null) {
                    afterPopTransactionRunnable.run();
                }
            }
        });
    }

    /**
     * Dispatch the pop-event. Priority of the top of the stack of Fraction
     * @param activeFraction active fraction instance
     * @return true on handled by active fraction
     */
    boolean dispatchBackPressedEvent(ISupportFraction activeFraction) {
        if (activeFraction != null) {
            boolean result = activeFraction.onBackPressedSupport();
            if (result) {
                return true;
            }
        }
        return false;
    }

    void handleResultRecord(FractionEx from) {
        try {
            PacMap args = from.getArguments();
            if (args == null) return;
            Optional<Sequenceable> oResultRecord = args.getSequenceable(FRAGMENTATION_ARG_RESULT_RECORD);
            ResultRecord resultRecord = oResultRecord.isPresent() ? (ResultRecord) (oResultRecord.get()) : null;
            if (resultRecord == null) return;
            Optional<Object> ofraction = from.getArguments().getObjectValue(FRAGMENTATION_ARG_RESULT_ASKING_FRACTION);
            ISupportFraction targetFraction = ofraction.isPresent() ? (ISupportFraction) ofraction.get() : null;
            if (targetFraction != null) {
                targetFraction.onFractionResult(resultRecord.requestCode, resultRecord.resultCode, resultRecord.resultBundle);
            }
        } catch (IllegalStateException ignored) {
            // Fraction no longer exists
        }
    }

    private void enqueue(FractionManagerEx fm, Action action) {
        if (fm == null) {
//            Log.w(TAG, "FractionManagerEx is null, skip the action!");
            return;
        }
        mActionQueue.enqueue(action);
    }

    private void doDispatchStartTransaction(FractionManagerEx fm, ISupportFraction from, ISupportFraction to, int requestCode, int launchMode, int type) {
        checkNotNull(to, "toFraction == null");
        FractionEx toex = fm.checkNewRecordOrAdd((Fraction) to);
        FractionEx fromex = fm.checkNewRecordOrAdd((Fraction) from);
        bindArguments(toex);
        if ((type == TYPE_ADD_RESULT || type == TYPE_ADD_RESULT_WITHOUT_HIDE) && from != null) {
            saveRequestCode(fromex, toex, requestCode);
        }

        from = getTopFractionForStart(from, fm);

        int containerId = getArguments(toex).getIntValue(FRAGMENTATION_ARG_CONTAINER, 0);
        if (from == null && containerId == 0) {
            LogUtil.error(TAG, "There is no Fraction in the FractionManagerEx, maybe you need to call loadRootFraction()!");
            return;
        }

        if (from != null && containerId == 0) {
            bindContainerId(from.getSupportDelegate().mContainerId, toex);
        }

        // process ExtraTransaction
        String toFractionTag = to.getClass().getName();
        boolean dontAddToBackStack = false;

        if (handleLaunchMode(fm, from, to, toFractionTag, launchMode)) return;

        start(fm, from, to, toFractionTag, dontAddToBackStack, type);
    }

    private void bindArguments(FractionEx toex) {
        ISupportFraction fraction = (ISupportFraction) toex.getFraction();
        PacMap newArgs = fraction.getSupportDelegate().mArgs;
        PacMap args = toex.getArguments();
        if ((newArgs != null) && (newArgs != args)) {
            args.putAll(newArgs);
            fraction.setArguments(args);
        }
    }

    private ISupportFraction getTopFractionForStart(ISupportFraction from, FractionManagerEx fm) {
        ISupportFraction top;
        if (from == null) {
            top = SupportHelper.getTopFraction(fm);
        } else {
            top = SupportHelper.getTopFraction(fm, from.getSupportDelegate().mContainerId);
        }
        return top;
    }

    private void start(FractionManagerEx fm, final ISupportFraction from, ISupportFraction to, String toFractionTag,
                       boolean dontAddToBackStack, int type) {
        FractionSchedulerEx ft = fm.startFractionScheduler();
        boolean addMode = (type == TYPE_ADD || type == TYPE_ADD_RESULT || type == TYPE_ADD_WITHOUT_HIDE || type == TYPE_ADD_RESULT_WITHOUT_HIDE);
        FractionEx fromF = fm.findActiveFractionEx((Fraction) from);
        FractionEx toF = fm.checkNewRecordOrAdd((Fraction) to, toFractionTag);
        PacMap args = getArguments(toF);
        args.putBooleanValue(FRAGMENTATION_ARG_REPLACE, !addMode);

        if (from == null) {
            ft.replace(args.getIntValue(FRAGMENTATION_ARG_CONTAINER), toF, toFractionTag);
        } else {
            if (addMode) {
                ft.add(from.getSupportDelegate().mContainerId, toF.getFraction(), toFractionTag);
                if (type != TYPE_ADD_WITHOUT_HIDE && type != TYPE_ADD_RESULT_WITHOUT_HIDE) {
                    ft.hide(fromF);
                }
            } else {
                ft.replace(from.getSupportDelegate().mContainerId, toF, toFractionTag);
            }
        }

        if (!dontAddToBackStack && type != TYPE_REPLACE_DONT_BACK) {
            ft.pushIntoStack(toFractionTag);
        }
        supportCommit(ft);
    }

    private void doShowHideFraction(FractionManagerEx fm, ISupportFraction showFraction, ISupportFraction hideFraction) {
        if (showFraction == hideFraction) return;

        FractionSchedulerEx ft = fm.startFractionScheduler().show((Fraction) showFraction);

        if (hideFraction == null) {
            List<FractionEx> fractionList = fm.getActiveFractions();
            if (fractionList != null) {
                for (FractionEx fraction : fractionList) {
                    if (fraction != null && fraction != showFraction) {
                        ft.hide(fraction.getFraction());
                        fraction.setHide();
                    }
                }
            }
        } else {
            ft.hide((Fraction) hideFraction);
        }
        supportCommit(ft);
    }

    private void bindContainerId(int containerId, FractionEx to) {
        PacMap args = to.getArguments();
        args.putIntValue(FRAGMENTATION_ARG_CONTAINER, containerId);
    }

    private PacMap getArguments(FractionEx fractionex) {
        PacMap bundle = fractionex.getArguments();
        if (bundle == null) {
            bundle = new PacMap();
            fractionex.setArguments(bundle);
        }
        return bundle;
    }

    private void supportCommit(FractionSchedulerEx transaction) {
        transaction.submit();
    }

    private boolean handleLaunchMode(FractionManagerEx fm, ISupportFraction topFraction, final ISupportFraction to, String toFractionTag, int launchMode) {
        if (launchMode == ISupportFraction.LAUNCHMODE_SINGLETOP || launchMode == ISupportFraction.LAUNCHMODE_SINGLETASK) {
            if (topFraction == null) return false;
            final ISupportFraction stackToFraction = SupportHelper.findBackStackFraction(to.getClass(), toFractionTag, fm);
            if (stackToFraction == null) return false;
            if (launchMode == ISupportFraction.LAUNCHMODE_SINGLETOP) {
                if (to == topFraction || to.getClass().getName().equals(topFraction.getClass().getName())) {
                    handleNewBundle(fm, to, stackToFraction);
                    return true;
                }
            } else if (launchMode == ISupportFraction.LAUNCHMODE_SINGLETASK) {
                doPopTo(toFractionTag, false, fm);
                mHandler.postTask(() -> handleNewBundle(fm, to, stackToFraction));
                return true;
            }
        }
        return false;
    }

    private void handleNewBundle(FractionManagerEx fm, ISupportFraction toFraction, ISupportFraction stackToFraction) {
        PacMap argsNewBundle = toFraction.getSupportDelegate().mNewBundle;

        PacMap args = getArguments(fm.checkNewRecordOrAdd((Fraction) toFraction));
        if (args.hasKey(FRAGMENTATION_ARG_CONTAINER)) {
            args.remove(FRAGMENTATION_ARG_CONTAINER);
        }
        if (argsNewBundle != null) {
            args.putAll(argsNewBundle);
        }
        stackToFraction.onNewBundle(args);
    }

    /**
     * save requestCode
     * @param fm fraction manager instance
     * @param from from fraction
     * @param to to fraction
     * @param requestCode specify action code
     */
    private void saveRequestCode(FractionEx from, FractionEx to, int requestCode) {
        PacMap bundle = getArguments(to);
        ResultRecord resultRecord = new ResultRecord();
        resultRecord.requestCode = requestCode;
        bundle.putSequenceableObject(FRAGMENTATION_ARG_RESULT_RECORD, resultRecord);
        to.getArguments().putObjectValue(FRAGMENTATION_ARG_RESULT_ASKING_FRACTION, from.getFraction());
    }

    private void doPopTo(final String targetFractionTag, boolean includeTargetFraction, FractionManagerEx fm) {
        FractionEx targetFractionex = fm.findFractionByTag(targetFractionTag);
        if (targetFractionex == null) {
            return;
        }

        int flag = 0;

        List<Fraction> willPopFractions = SupportHelper.getWillPopFractions(fm, targetFractionTag, includeTargetFraction);

        if (willPopFractions.size() <= 0) return;

        Fraction top = willPopFractions.get(0);
        mockPopToAnim(top, fm, willPopFractions);
        if (!includeTargetFraction) {
            fm.startFractionScheduler()
                    .show(targetFractionex)
                    .submit();
        } else{
            FractionEx top2 = SupportHelper.getBackStackTopFractionEx(fm);
            if (top2 != null) {
                fm.startFractionScheduler()
                        .show(top2)
                        .submit();
            }
        }
    }

    private void safePopTo(final FractionManagerEx fm, List<Fraction> willPopFractions) {
        FractionScheduler transaction = fm.startFractionScheduler();
        for (Fraction fraction : willPopFractions) {
            transaction.remove(fraction);
        }
        transaction.submit();
    }

    private void mockPopToAnim(Fraction from, FractionManagerEx fm, List<Fraction> willPopFractions) {
        if (!(from instanceof ISupportFraction)) {
            safePopTo(fm, willPopFractions);
            return;
        }
        safePopTo(fm, willPopFractions);
    }

    private ComponentContainer findContainerById(FractionEx fraction, int containerId) {
        if (fraction.getFraction().getComponent() == null) return null;

        Component container;
        container = mAbility.findComponentById(containerId);

        if (container instanceof ComponentContainer) {
            return (ComponentContainer) container;
        }

        return null;
    }

    private static <T> void checkNotNull(T value, String message) {
        if (value == null) {
            throw new NullPointerException(message);
        }
    }
}
